#!/usr/bin/env python
# -*-*- encoding: utf-8 -*-*-
#
# Copyright (C) 2008 Håvard Gulldahl.
#
# Licensed under the GNU Public License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.gnu.org/licenses/gpl-2.0.txt
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""A desktop client for ContactsWeb services."""

DEBUG = True
INTERNAL_MIMETYPE = 'application/vnd.gcontactsbox.contact'
import logging

import gdata.contacts.service as service
import gcontacts.gcontactsbase as contacts

from PyQt4 import QtCore, QtGui

import atom
atom.XML_STRING_ENCODING=None # workaround bug

class EntryList(object):
  l = []
  g = None
  def add(self, entry):
    newid = entry.id.text
    for en in self.l:
      if en.id.text == newid:
        self.l.remove(en)
        break
    entry.groups = [g.href for g in entry.group_membership_info]
    self.l.append(entry)

  def switch(self, group=None):
    logging.debug('setting group: %s', group)
    self.g = group

  def in_group(self, entry):
    #logging.debug('is entry (%s) in group %s?', entry.groups, self.g)
    if self.g is None: # no group, show all entries
      return True
    else:
      try:
        return self.g in entry.groups
      except AttributeError:
        return False

  def pop(self, i):
    return self.l.pop(i)

  def get(self, i):
    return self.l[i]

  def popid(self, entryid):
    for i, e in enumerate(self.l):
      if e.id.text == entryid:
        return self.pop(i)
    return False

  def __len__(self):
    return len(self.l)

class ContactsListModel(QtCore.QAbstractTableModel):
  entries = EntryList()
  groups = {}
  total_results = 0

  internal_mimetype = INTERNAL_MIMETYPE
  vcard_mimetypes = ['text/directory', 'text/x-vcard', 'text/directory;charset="utf-8";profile="vCard"', ]
  text_mimetypes = ['text/plain', ]
  moz_mimetypes = ['text/x-moz-address', 'text/x-moz-url']
  uri_mimetypes = ['text/uri-list', ]
  file_suffixes = ['.contact', '.vcf']
  dragStartPos = QtCore.QPoint()

  def doLoadContactsFeed(self, feed, feed_start_idx, feed_length):
    #print 'doLoadContactsFeed', feed, feed_start_idx, feed_length
    self.emit(QtCore.SIGNAL("layoutAboutToBeChanged()"))
    self.total_results = int(feed.total_results.text)
    for e in feed.entry:
      self.entries.add(e)
    self.emit(QtCore.SIGNAL("layoutChanged()"))

  def doLoadContactEntries(self, entries, feed_start_idx=0):
    self.emit(QtCore.SIGNAL("layoutAboutToBeChanged()"))
    self.total_results += len(entries)
    for e in entries:
      self.entries.add(e)
    self.emit(QtCore.SIGNAL("layoutChanged()"))
    # Find new entries
    newentries = []
    for e in entries:
      newentries += self.match(QtCore.QModelIndex(), QtCore.Qt.DisplayRole,
                      QtCore.QVariant(e.PrettyName().decode('utf8').title()),
                      len(entries), QtCore.Qt.MatchStartsWith)
    logging.debug('Found matches: %s', len(newentries))
    if len(newentries):
      self.emit(QtCore.SIGNAL("rowsAdded(PyQt_PyObject)"), newentries)

  def doDeleteIndexes(self, indexes):
    self.emit(QtCore.SIGNAL("layoutAboutToBeChanged()"))
    _items = set([idx.row() for idx in indexes])
    for i in _items:
      item = self.entries.pop(i)
      logging.debug('deleting %s', item.PrettyName())
      self.emit(QtCore.SIGNAL("deleteContact(PyQt_PyObject)"), item)
    self.emit(QtCore.SIGNAL("layoutChanged()"))
    self.total_results -= 1

  def doRemoveContact(self, contact):
    self.emit(QtCore.SIGNAL("layoutAboutToBeChanged()"))
    self.entries.popid(contact.id.text)
    self.emit(QtCore.SIGNAL("layoutChanged()"))
    self.total_results -= 1

  def doRemoveContacts(self, contacts):
    self.emit(QtCore.SIGNAL("layoutAboutToBeChanged()"))
    for contact in contacts:
      logging.debug('removing contact %s' , contact.id.text)
      self.entries.popid(contact.id.text)
      self.total_results -= 1
    self.emit(QtCore.SIGNAL("layoutChanged()"))

  #def removeRows(self, firstrow, count, index=None):
    #parentindex = QtCore.QModelIndex()
    #self.emit(QtCore.SIGNAL("beginRemoveRows(QModelIndex, int, int)"), parentindex, firstrow, firstrow+count)
    #self.entries.remove(firstrow, count)
    #self.emit(QtCore.SIGNAL("endRemoveRows()"))
    #self.total_results -= 1

  def doLoadGroup(self, group=None):
    self.emit(QtCore.SIGNAL("layoutAboutToBeChanged()"))
    self.entries.switch(group)
    self.emit(QtCore.SIGNAL("layoutChanged()"))

  def doParseGroupFeed(self, groupfeed):
    for group in groupfeed.entry:
      self.groups[group.id.text] = group.title.text

  def rowCount(self, parentidx):
    return len(self.entries)

  def data(self, idx, role):
    #print "data", idx, role

    #The general purpose roles are:
    #Qt::DisplayRole  0 The key data to be rendered (usually text).
    #Qt::DecorationRole 1 The data to be rendered as a decoration (usually an icon).
    #Qt::EditRole 2 The data in a form suitable for editing in an editor.
    #Qt::ToolTipRole  3 The data displayed in the item's tooltip.
    #Qt::StatusTipRole  4 The data displayed in the status bar.
    #Qt::WhatsThisRole  5 The data displayed for the item in "What's This?" mode.
    #Qt::SizeHintRole 13  The size hint for the item that will be supplied to views.

    #Roles describing appearance and meta data:
    #Qt::FontRole 6 The font used for items rendered with the default delegate.
    #Qt::TextAlignmentRole  7 The alignment of the text for items rendered with the default delegate.
    #Qt::BackgroundRole 8 The background brush used for items rendered with the default delegate.
    #Qt::BackgroundColorRole  8 This role is obsolete. Use BackgroundRole instead.
    #Qt::ForegroundRole 9 The foreground brush (text color, typically) used for items rendered with the default delegate.
    #Qt::TextColorRole  9 This role is obsolete. Use ForegroundRole instead.
    #Qt::CheckStateRole 10  This role is used to obtain the checked state of an item (see Qt::CheckState).

    #Accessibility roles:
    #Qt::AccessibleTextRole 11  The text to be used by accessibility extensions and plugins, such as screen readers.
    #Qt::AccessibleDescriptionRole  12  A description of the item for accessibility purposes.

    #User roles:
    #Qt::UserRole 32  The first role that can be used for application-specific purposes.

    if not idx.isValid():
      return QtCore.QVariant()

    try:
      item = self.entries.get(idx.row())
    except IndexError:
      return QtCore.QVariant()

    if role == QtCore.Qt.DisplayRole:
      try:
        if idx.column() == 0:
          #t = item.title.text.decode('utf8')
          t = item.PrettyName().decode('utf8').title()
        elif idx.column() == 1:
          t = ", ".join([ e.address.decode('utf8') for e in item.email ])
        elif idx.column() == 2:
          t = item.updated.text
      except AttributeError:
        t = "..."
      return QtCore.QVariant(t)
    elif role == QtCore.Qt.DecorationRole:
      icon = item.GetPhotoLink()
      if icon is not None and False:
        logging.debug('icon: %s', icon)
      return QtCore.QVariant()
      #try:
        #thumb = self.makeThumbnail(item.entry)
        #return QtCore.QVariant(thumb)
      #except (AttributeError, TypeError):
        #return QtCore.QVariant()
    elif role == QtCore.Qt.ToolTipRole:
      try:
        t = """<b>Name</b>: %s<br>
       <b>E-mail</b>: %s<br>
       <b>Last updated</b>: %s """ % (item.PrettyName().decode('utf8'),
                                       ",".join([e.address for e in item.email]),
                                       item.updated.text.replace('T', ' '), )
        if item.phone_number:
          t += "<br><b>Phone</b>: %s" % ",".join([n.text for n in item.phone_number])
        if item.im:
          t += "<br><b>IM</b>:<ul>"
          for im in item.im:
            t += "<li><img src='%s' width='32' height='32'>%s</li>" % (imProtocolIcon(im.protocol), im.address)
          t += "</ul>"
        if item.groups:
          t += "<br><b>Groups</b>: %s" % ",".join([self.groups[g] for g in item.groups])
      except AttributeError:
        t = ''
      return QtCore.QVariant(t)
    else:
      return QtCore.QVariant()

  def headerData(self, section, orientation, role):
    #print "headerData", section, orientation, role
    if role != QtCore.Qt.DisplayRole:
      return QtCore.QVariant()

    if orientation == QtCore.Qt.Horizontal:
      if section == 0:
        return QtCore.QVariant('Contact name')
      elif section == 1:
        return QtCore.QVariant('Emails')
      elif section == 2:
        return QtCore.QVariant('Updated')
    else:
      return QtCore.QVariant('%s' % section)

  def flags(self, idx):
    if not idx.isValid():
         return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDropEnabled
    item = self.entries.get(idx.row())
    if self.entries.in_group(item):
      return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled
    else:
      return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDropEnabled

  def canFetchMore(self, idx):
    return len(self.entries) < self.total_results

  def fetchMore(self, parent):
    logging.debug('fetchmore')
    self.emit(QtCore.SIGNAL('getContacts(PyQt_PyObject,PyQt_PyObject)'), len(self.entries), 75 )

  def columnCount(self, parentidx):
    return 2

  def supportedDropActions(self):
    return QtCore.Qt.CopyAction

  def mimeTypes(self):
    #logging.debug('mimetypes %s', self.vcard_mimetypes + self.text_mimetypes + self.moz_mimetypes + self.uri_mimetypes + [self.internal_mimetype, ])
    return self.vcard_mimetypes + self.text_mimetypes + self.uri_mimetypes + [self.internal_mimetype, ] + self.moz_mimetypes

  def mimeData(self, indexes):
    items = list(set([ self.entries.get(idx.row()) for idx in indexes ])) # hack to uniquify
    logging.debug("mimeData %s", items)
    mime = QtCore.QAbstractTableModel.mimeData(self, indexes)
    vcards = emails = ''
    feed = contacts.ContactsFeed(entry = items)
    mime.setData(self.internal_mimetype, feed.ToString())
    for item in items:
      vcards += item.ToVCard() + '\r\n'
      #logging.debug('adding card: %s (%s)', repr(vcard), type(vcard))
      try: _e = item.email[0].address # HACK: get first email
      except IndexError: _e = ''
      emails += '%s <%s>' % (item.title.text, _e) + ', '
    for mt in self.vcard_mimetypes:
      mime.setData(mt, vcards)
    mime.setText(emails)
    logging.debug("mimeData return %s", map(str, mime.formats()))
    return mime

  def dropMimeData(self, data, action, row, column, parent):
    logging.debug("drop, action: %s", action)
    mime =  data
    logging.debug("found mime %s", map(str, data.formats()))
    if self.internal_mimetype in map(str, data.formats()):
      return False  # internal drop
    for t in self.vcard_mimetypes: # look at mime type
      logging.debug("looking at vcard: %s", t)
      if mime.hasFormat(t):
        logging.debug("yes")
        vcardData = mime.data(t)
        #logging.debug(vcardData)
        self.emit(QtCore.SIGNAL('droppedVCard(PyQt_PyObject)'), unicode(vcardData, 'utf-8'))
        return True
    for t in self.moz_mimetypes: # look at mime type
      logging.debug('Drop has moz urls')
      logging.debug("looking at %s", t)
      if mime.hasFormat(t):
        textaddress = unicode(mime.data(t),  'utf-16').replace('\x00',  '')
        self.emit(QtCore.SIGNAL('droppedAddress(PyQt_PyObject)'), textaddress)
        return True
    if mime.hasUrls(): # look at the source url
      logging.debug('Drop has urls: %s', [unicode(u.toString()) for u in mime.urls()])
      for url in mime.urls():
        for s in self.file_suffixes:
          if s in unicode(url.toString()):
            self.emit(QtCore.SIGNAL('droppedUrl(PyQt_PyObject)'), url)
            return True
    if mime.hasText(): # look for "name <address@example.com>" and "address@example.com (name)"
      logging.debug('Drop has text: %s', unicode(mime.text()))
      textaddress = unicode(mime.text(),  'utf-8')
      if '@' in textaddress and '.' in textaddress:
        self.emit(QtCore.SIGNAL('droppedAddress(PyQt_PyObject)'), textaddress)
        return True
    logging.warning('The drop was not taken care of')
    return False

def imProtocolIcon(protocol):
  #logging.debug('finding icon for im protocol %s', protocol)
  icons = {'http://schemas.google.com/g/2005#AIM': ':/graphics/im-aim.svg',
            'http://schemas.google.com/g/2005#ICQ': ':/graphics/im-icq.svg',
            'http://schemas.google.com/g/2005#MSN': ':/graphics/im-msn.svg',
            'http://schemas.google.com/g/2005#JABBER': ':/graphics/im-jabber.svg',
            'http://schemas.google.com/g/2005#GOOGLE_TALK': ':/graphics/emblem-new.svg',
            'http://schemas.google.com/g/2005#YAHOO': ':/graphics/im-yahoo.svg',
            'http://schemas.google.com/g/2005#SKYPE': ':/graphics/person.svg',
            'http://schemas.google.com/g/2005#QQ': ':/graphics/person.svg',
          }
  return icons.get(protocol, ':/graphics/person.svg')
